package com.lagou.homework;

import com.lagou.homework.annotation.AnnotationMetadata;
import com.lagou.homework.annotation.AnnotationsScanner;
import com.lagou.homework.annotation.ComponentScan;
import com.lagou.homework.support.BeanDefinition;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author boting.guo
 * @date 2021/9/18 23:55
 */

public class BeanDefinitionScanner {

    private static final String COMPONENT_SCAN_ANNOTATION_CLASSNAME = "com.lagou.homework.annotation.ComponentScan";
    private static final String COMPONENT_ANNOTATION_CLASSNAME = "com.lagou.homework.annotation.Component";

    private final Map<String, Set<String>> metaAnnotationTypesCache = new HashMap<>(16);

    private DefaultListableBeanFactory registry;

    public BeanDefinitionScanner(DefaultListableBeanFactory registry) {
        this.registry = registry;
    }

    public void scan(String[] basePackages) throws Exception {
        Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            Set<Class<?>> candidates = findCandidateComponents(basePackage);
            this.registry.register(candidates.toArray(Class<?>[]::new));
        }
    }

    private Set<Class<?>> findCandidateComponents(String basePackage) throws IOException, ClassNotFoundException {
        Set<Class<?>> candidates = new LinkedHashSet<>();
        Set<Class<?>> classes = scanPackage(basePackage);

        for (Class<?> clazz : classes) {
            Annotation[] declaredAnnotations = AnnotationsScanner.getDeclaredAnnotations(clazz);
            for (Annotation annotation : declaredAnnotations) {
                Set<String> metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotation.annotationType().getName(), key -> {
                    List<Annotation> mergedAnnotations = AnnotationsScanner.getMergedAnnotations(annotation.annotationType());
                    return mergedAnnotations.isEmpty() ? Collections.emptySet() :
                            mergedAnnotations.stream().map(anno -> anno.annotationType().getName())
                                    .collect(Collectors.toCollection(LinkedHashSet::new));
                });

                if (isStereotypeWithNameValue(annotation.annotationType().getName(), metaTypes)) {
                    candidates.add(clazz);
                }
            }
        }
        return candidates;
    }


    protected boolean isStereotypeWithNameValue(String annotationType, Set<String> metaAnnotationTypes) {
        return annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) ||
                metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME);
    }

    private Set<Class<?>> scanPackage(String basePackage) throws IOException, ClassNotFoundException {
        Set<Class<?>> classes = new LinkedHashSet<>();
        String packageDirName = basePackage.replace('.', '/');
        Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            String protocol = url.getProtocol();
            if ("file".equals(protocol)) {
                String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
                scanClassesFile(basePackage, filePath, classes);
            }
        }
        return classes;
    }

    private void scanClassesFile(String pkgName, String pkgPath, Set<Class<?>> classes) throws ClassNotFoundException {
        File dir = new File(pkgPath);
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }

        File[] dirFiles = dir.listFiles(pathname -> pathname.isDirectory() || pathname.getName().endsWith("class"));

        if (dirFiles == null || dirFiles.length == 0) {
            return;
        }

        String className;
        Class<?> clazz;
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        for (File f : dirFiles) {
            if (f.isDirectory()) {
                scanClassesFile(pkgName + "." + f.getName(), pkgPath + "/" + f.getName(), classes);
                continue;
            }
            className = f.getName();
            className = className.substring(0, className.length() - 6);

            clazz = classLoader.loadClass(pkgName + "." + className);
            if (clazz != null) {
                classes.add(clazz);
            }
        }
    }

    public void scan(BeanDefinition beanDefinition) throws Exception {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        Set<String> annotationTypes = metadata.getAnnotationTypes();
        for (String annotationType : annotationTypes) {
            if (annotationType.equals(COMPONENT_SCAN_ANNOTATION_CLASSNAME)) {
                ComponentScan annotation = metadata.getIntrospectedClass().getAnnotation(ComponentScan.class);
                Properties attributes = metadata.attributesFor(annotationType);
                String[] value = (String[]) attributes.get("value");
                if (value.length == 0) {
                    String basePackage = ((Class<?>) beanDefinition.getBeanClass()).getPackageName();
                    value = new String[]{basePackage};
                }
                this.scan(value);
            }
        }
    }
}